/**
 * Copyright (c) 2017-2018, zengjintao (1913188966@qq.com).
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jfast.framework.orm.query;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.jfast.framework.orm.Page;
import com.jfast.framework.orm.TableInfo;
import org.apache.commons.beanutils.PropertyUtils;
import com.jfast.framework.web.orm.ModelBuilder;
import com.jfast.framework.exception.DataSourceException;
import com.jfast.framework.log.Logger;
import com.jfast.framework.orm.config.DataSourceManager;
import com.jfast.framework.orm.config.DbConfig;
import com.jfast.framework.orm.config.TableMapping;
import com.jfast.framework.orm.dialect.Dialect;

public class DefaultJdbcTemplate implements JdbcTemplate {

    static final Object[] NULL_PARA_ARRAY = new Object[0];
    
    private static final Logger logger = Logger.getLogger(DefaultJdbcTemplate.class);
	
	private static final ModelBuilder modelBuilder = ModelBuilder.me();
	
	public <T> List<T> findAll(String sql, Class<T> cla, Object...  params){
		String configName = getConfigName(cla);
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		return find(dbConfig, sql, cla, params);
	}
	
	public <T> List<T> findAll(String sql, Class<T> cla){
		String configName = getConfigName(cla);
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		return find(dbConfig, sql, cla);
	}
	
	@SuppressWarnings({ "rawtypes"})
	private String getConfigName(Class cla){
		TableMapping tableMapping = DataSourceManager.getTableMappingMap(cla.getPackage().getName());
		return tableMapping.getConfigName();
	}
	
	private <T> List<T> find(DbConfig dbConfig, String sql, Class<T> cla){
		Connection connection = null;
		try {
			connection = dbConfig.getConnection();
			PreparedStatement preparedStatement = connection.prepareStatement(sql);
			ResultSet resultSet = preparedStatement.executeQuery();
			List<T> modelList = modelBuilder.builderModelList(resultSet, cla);
			return modelList;
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			dbConfig.close(connection);
		}
	}
	
	private  Map<String, Object> findToMap(String sql, DbConfig dbConfig, Object... params){
		Connection connection = null;
		try {
			connection = dbConfig.getConnection();
			PreparedStatement preparedStatement = connection.prepareStatement(sql);
			modelBuilder.fillStatement(preparedStatement, params);
			ResultSet resultSet = preparedStatement.executeQuery();
			Map<String, Object> modelMap = modelBuilder.builderToMap(resultSet);
			return modelMap;
		} catch (SQLException e) {
			throw new DataSourceException(e);
		} finally {
			dbConfig.close(connection);
		}
	}

	public <T> T findOne(String sql, Class<T> cla){
		List<T> result = findAll(sql, cla, NULL_PARA_ARRAY);
		return result.size() > 0 ? result.get(0) : null;
	}
	
	public <T> T findOne(String sql, Class<T> cla, Object... params) {
		List<T> result = findAll(sql, cla, params);
		return result.size()> 0 ? result.get(0) : null;
	}
	
	private  <T> List<T> find(DbConfig dbConfig, String sql, Class<T> cla, Object... params){
		Connection connection = null;
		try {
			connection = dbConfig.getConnection();
			PreparedStatement preparedStatement = connection.prepareStatement(sql);
			modelBuilder.fillStatement(preparedStatement, params);
			ResultSet resultSet = preparedStatement.executeQuery();
			List<T> modelList = modelBuilder.builderModelList(resultSet, cla);
			return modelList;
		} catch (Exception e) {
			throw new DataSourceException(e);
		} finally {
			dbConfig.close(connection);
		}
	}
	
	public boolean update(Object object){
		String configName = getConfigName(object.getClass());
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		return update(dbConfig, object);
	}

	private  boolean update(DbConfig dbConfig, Object object) {
		Connection connection = null;
		try {
			String configName = dbConfig.getDataSourceFactory().getDataSourceConfig().getBasePackage();
			TableMapping tableMapping = DataSourceManager.getTableMappingMap(configName);
			TableInfo tableInfo = tableMapping.getTableInfo(object.getClass());
			Map<String,Object> columnAttr = new HashMap<>();
			for (Entry<String, String> entry : tableInfo.getColumnMapping().entrySet()) {
				columnAttr.put(entry.getValue(), PropertyUtils.getProperty(object, entry.getKey()));
			}
			Dialect dialect = DataSourceManager.getDialect(dbConfig);
			List<Object> params = new ArrayList<Object>();
			StringBuilder sql = new StringBuilder();
			dialect.forModelSave(sql, columnAttr, tableInfo.getTableName(), params);
			connection = dbConfig.getConnection();
			return update(sql.toString(),tableInfo.getPrimaryKeys(),connection, params) >= 1;
		} catch (Exception e) {
			throw new DataSourceException(e);
		} finally {
			dbConfig.close(connection);
		}
	}

	private int update(String sql, String[] primaryKeys, Connection connection, List<Object> paras) throws SQLException {
		PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
		modelBuilder.fillStatement(statement, paras.toArray());
		return statement.executeUpdate();
	}
	
	public <T> Page<T> paginate(int pageNumber, int pageSize, String select, String sqlContent, Class<T> clazz, Object... params){
		String configName = getConfigName(clazz);
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		String countSql = "select count(*) as count" + sqlContent + " limit 1";
		Connection connection = null;
		int totalRow = 0;
		try {
		    connection = dbConfig.getConnection();
			PreparedStatement preparedStatement = connection.prepareStatement(countSql);
			modelBuilder.fillStatement(preparedStatement, params);
			ResultSet resultSet = preparedStatement.executeQuery();
			while (resultSet.next()) {
				totalRow = resultSet.getInt("count");
			}
			int totalPage = (int) (totalRow / pageSize);
			if (totalRow % pageSize != 0) {
				totalPage++;
			}
			if (pageNumber > totalPage) {
				return new Page<T>(new ArrayList<T>(0), pageNumber, pageSize, totalPage, (int)totalRow);
			}
			Dialect dialect = DataSourceManager.getDialect(dbConfig);
			String sql = dialect.forPaginate(pageNumber, pageSize, select + sqlContent);
			List<T> list = find(dbConfig, sql, clazz, params);
			return  new Page<T>(list, pageNumber, pageSize, totalPage, totalRow);
		} catch (SQLException e) {
			logger.error(e.getMessage(), e);
			throw new DataSourceException(e);
		}
	}

	@Override
	public DbConfig getDbConfig(String configName) {
		return DataSourceManager.getDbConfig(configName);
	}

	@Override
	public boolean insert(Object object) {
		return update(object);
	}

	@Override
	public DbConfig getDefaultDbConfig() {
	    return DataSourceManager.getDefaultDbConfig();
	}
	
	@Override
	public Map<String, Object> findMap(String sql) {
	    DbConfig dbConfig = DataSourceManager.getDbConfig(DataSourceManager.DEFAULT_DATASOURCE_NAME);
	    return findToMap(sql, dbConfig, NULL_PARA_ARRAY);
	}
	
	@Override
	public Map<String, Object> findMap(String sql, Object... params) {
		DbConfig dbConfig = DataSourceManager.getDbConfig(DataSourceManager.DEFAULT_DATASOURCE_NAME);
		return findToMap(sql, dbConfig, params);
	}


	@Override
	public Map<String, Object> findMap(String sql, String configName) {
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		return findToMap(sql, dbConfig, NULL_PARA_ARRAY);
	}

	@Override
	public Map<String, Object> findMap(String sql, String configName, Object... params) {
		DbConfig dbConfig = DataSourceManager.getDbConfig(configName);
		return findToMap(sql, dbConfig, params);
	}
}
